"""
Call graph analyzer for Python code.

Analyzes function calls, builds call graphs, and performs dependency analysis
for Python code structures.
"""

from typing import Dict, List, Any, Optional, Set, Tuple
from collections import defaultdict, deque
import re

from ...core.error_handler import AnalysisError, log_info


class CallGraphAnalyzer:
    """Analyzes Python code to build call graphs and understand dependencies."""
    
    def __init__(self):
        """Initialize the call graph analyzer."""
        self.call_cache = {}
        self.visited_functions = set()
    
    def analyze_function_calls(self, function_info: Dict[str, Any], 
                              analysis: Dict[str, Any], max_depth: int = 10) -> Dict[str, Any]:
        """Analyze function calls starting from a specific function."""
        self.call_cache.clear()
        self.visited_functions.clear()
        
        # Build function lookup tables
        function_lookup = {f['name']: f for f in analysis.get('functions', [])}
        method_lookup = self._build_method_lookup(analysis.get('classes', []))
        
        # Start call graph generation
        call_graph = self._build_function_call_graph(
            function_info, function_lookup, method_lookup, max_depth, 0
        )
        
        return {
            'root_function': function_info['name'],
            'graph': call_graph,
            'total_calls': self._count_calls(call_graph),
            'max_depth': self._calculate_max_depth(call_graph),
            'unique_functions': list(self.visited_functions),
            'statistics': self._generate_statistics(call_graph)
        }
    
    def _build_method_lookup(self, classes: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
        """Build lookup table for class methods."""
        method_lookup = {}
        
        for cls in classes:
            class_methods = {}
            for method in cls.get('methods', []):
                class_methods[method['name']] = method
            method_lookup[cls['name']] = class_methods
        
        return method_lookup
    
    def _build_function_call_graph(self, function_info: Dict[str, Any], 
                                  function_lookup: Dict[str, Dict[str, Any]],
                                  method_lookup: Dict[str, Dict[str, Any]],
                                  max_depth: int, current_depth: int) -> Dict[str, Any]:
        """Recursively build call graph for a function."""
        function_name = function_info['name']
        
        # Avoid infinite recursion
        if current_depth >= max_depth:
            return {
                'name': function_name,
                'type': 'function',
                'depth': current_depth,
                'calls': [],
                'truncated': True,
                'info': function_info
            }
        
        # Track visited functions to detect cycles
        if function_name in self.visited_functions:
            return {
                'name': function_name,
                'type': 'function',
                'depth': current_depth,
                'calls': [],
                'recursive': True,
                'info': function_info
            }
        
        self.visited_functions.add(function_name)
        
        node = {
            'name': function_name,
            'type': 'function',
            'depth': current_depth,
            'calls': [],
            'info': function_info,
            'metadata': {
                'line_start': function_info.get('line_start'),
                'line_end': function_info.get('line_end'),
                'is_async': function_info.get('is_async', False),
                'is_method': function_info.get('is_method', False),
                'decorators': function_info.get('decorators', [])
            }
        }
        
        # Process function calls within this function
        for call in function_info.get('calls', []):
            call_node = self._process_call(
                call, function_lookup, method_lookup, max_depth, current_depth + 1
            )
            if call_node:
                node['calls'].append(call_node)
        
        return node
    
    def _process_call(self, call: Dict[str, Any], 
                     function_lookup: Dict[str, Dict[str, Any]],
                     method_lookup: Dict[str, Dict[str, Any]],
                     max_depth: int, current_depth: int) -> Optional[Dict[str, Any]]:
        """Process a single function call."""
        call_type = call.get('type', 'function_call')
        target = call.get('target')
        
        if not target:
            return None
        
        if call_type == 'function_call':
            # Look up function in the current file
            if target in function_lookup:
                target_function = function_lookup[target]
                return self._build_function_call_graph(
                    target_function, function_lookup, method_lookup, max_depth, current_depth
                )
            else:
                # External function call
                return {
                    'name': target,
                    'type': 'external_function',
                    'depth': current_depth,
                    'calls': [],
                    'info': {
                        'name': target,
                        'external': True,
                        'call_info': call
                    }
                }
        
        elif call_type == 'method_call':
            # Try to resolve method calls
            object_name = call.get('object', '')
            method_name = target
            
            # Try to find the method in known classes
            resolved_method = self._resolve_method_call(
                object_name, method_name, method_lookup, function_lookup
            )
            
            if resolved_method:
                return self._build_function_call_graph(
                    resolved_method, function_lookup, method_lookup, max_depth, current_depth
                )
            else:
                # External or unresolved method call
                return {
                    'name': f"{object_name}.{method_name}",
                    'type': 'external_method',
                    'depth': current_depth,
                    'calls': [],
                    'info': {
                        'name': method_name,
                        'object': object_name,
                        'external': True,
                        'call_info': call
                    }
                }
        
        elif call_type == 'complex_call':
            # Complex calls (function pointers, etc.)
            return {
                'name': target,
                'type': 'complex_call',
                'depth': current_depth,
                'calls': [],
                'info': {
                    'name': target,
                    'complex': True,
                    'call_info': call
                }
            }
        
        return None
    
    def _resolve_method_call(self, object_name: str, method_name: str,
                           method_lookup: Dict[str, Dict[str, Any]],
                           function_lookup: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        """Try to resolve a method call to a specific class method."""
        # Simple heuristics for method resolution
        
        # Case 1: object_name matches a class name
        if object_name in method_lookup:
            class_methods = method_lookup[object_name]
            if method_name in class_methods:
                return class_methods[method_name]
        
        # Case 2: self method call (object_name is 'self')
        if object_name == 'self':
            # Look for the method in all classes (would need better context)
            for class_name, class_methods in method_lookup.items():
                if method_name in class_methods:
                    return class_methods[method_name]
        
        # Case 3: Check if it's actually a function call misclassified
        if method_name in function_lookup:
            return function_lookup[method_name]
        
        return None
    
    def _count_calls(self, graph: Dict[str, Any]) -> int:
        """Count total number of calls in the graph."""
        count = len(graph.get('calls', []))
        for call in graph.get('calls', []):
            count += self._count_calls(call)
        return count
    
    def _calculate_max_depth(self, graph: Dict[str, Any]) -> int:
        """Calculate maximum depth of the call graph."""
        if not graph.get('calls'):
            return graph.get('depth', 0)
        
        max_child_depth = max(
            self._calculate_max_depth(call) for call in graph['calls']
        )
        return max_child_depth
    
    def _generate_statistics(self, graph: Dict[str, Any]) -> Dict[str, Any]:
        """Generate statistics about the call graph."""
        stats = {
            'total_nodes': 0,
            'function_calls': 0,
            'method_calls': 0,
            'external_calls': 0,
            'recursive_calls': 0,
            'max_depth': 0,
            'avg_calls_per_function': 0
        }
        
        def _collect_stats(node):
            stats['total_nodes'] += 1
            stats['max_depth'] = max(stats['max_depth'], node.get('depth', 0))
            
            if node.get('recursive'):
                stats['recursive_calls'] += 1
            
            node_type = node.get('type', '')
            if 'external' in node_type:
                stats['external_calls'] += 1
            elif node_type == 'function':
                stats['function_calls'] += 1
            elif 'method' in node_type:
                stats['method_calls'] += 1
            
            for call in node.get('calls', []):
                _collect_stats(call)
        
        _collect_stats(graph)
        
        if stats['total_nodes'] > 0:
            total_calls = stats['function_calls'] + stats['method_calls'] + stats['external_calls']
            stats['avg_calls_per_function'] = total_calls / stats['total_nodes']
        
        return stats
    
    def build_call_tree(self, call_graph: Dict[str, Any], include_external: bool = False) -> Dict[str, Any]:
        """Build a simplified call tree structure from the call graph."""
        def _build_tree_node(graph_node):
            node = {
                'name': graph_node['name'],
                'type': graph_node['type'],
                'depth': graph_node['depth'],
                'children': []
            }
            
            # Add metadata
            if 'info' in graph_node:
                info = graph_node['info']
                node['metadata'] = {
                    'line_start': info.get('line_start'),
                    'is_async': info.get('is_async', False),
                    'is_method': info.get('is_method', False),
                    'external': info.get('external', False)
                }
            
            # Add children
            for call in graph_node.get('calls', []):
                # Skip external calls if not requested
                if not include_external and 'external' in call.get('type', ''):
                    continue
                
                child_node = _build_tree_node(call)
                node['children'].append(child_node)
            
            return node
        
        tree = _build_tree_node(call_graph['graph'])
        
        # Calculate tree statistics
        def _count_nodes(node):
            count = 1
            for child in node.get('children', []):
                count += _count_nodes(child)
            return count
        
        def _calculate_depth(node):
            if not node.get('children'):
                return 1
            return 1 + max(_calculate_depth(child) for child in node['children'])
        
        tree_stats = {
            'node_count': _count_nodes(tree),
            'max_depth': _calculate_depth(tree),
            'external_calls': len([n for n in self._flatten_tree(tree) 
                                 if n.get('metadata', {}).get('external', False)]),
            'recursive_calls': call_graph.get('statistics', {}).get('recursive_calls', 0)
        }
        
        return {
            'tree': tree,
            'statistics': tree_stats,
            'root_function': call_graph['root_function']
        }
    
    def _flatten_tree(self, tree_node: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Flatten tree structure into a list of nodes."""
        nodes = [tree_node]
        for child in tree_node.get('children', []):
            nodes.extend(self._flatten_tree(child))
        return nodes
    
    def analyze_class_hierarchy(self, class_info: Dict[str, Any], 
                               analysis: Dict[str, Any]) -> Dict[str, Any]:
        """Analyze class inheritance hierarchy."""
        class_name = class_info['name']
        bases = class_info.get('bases', [])
        
        # Build class lookup
        class_lookup = {cls['name']: cls for cls in analysis.get('classes', [])}
        
        hierarchy = {
            'class_name': class_name,
            'base_classes': [],
            'derived_classes': [],
            'method_resolution_order': [],
            'inherited_methods': [],
            'overridden_methods': []
        }
        
        # Find base classes
        for base in bases:
            base_name = base.split('.')[-1]  # Handle qualified names
            if base_name in class_lookup:
                hierarchy['base_classes'].append({
                    'name': base_name,
                    'info': class_lookup[base_name]
                })
        
        # Find derived classes
        for cls in analysis.get('classes', []):
            if cls['name'] != class_name:
                for base in cls.get('bases', []):
                    base_name = base.split('.')[-1]
                    if base_name == class_name:
                        hierarchy['derived_classes'].append({
                            'name': cls['name'],
                            'info': cls
                        })
        
        # Analyze method inheritance and overriding
        current_methods = {m['name']: m for m in class_info.get('methods', [])}
        
        for base_class in hierarchy['base_classes']:
            base_methods = {m['name']: m for m in base_class['info'].get('methods', [])}
            
            for method_name, method_info in base_methods.items():
                if method_name in current_methods:
                    # Method is overridden
                    hierarchy['overridden_methods'].append({
                        'name': method_name,
                        'base_class': base_class['name'],
                        'base_method': method_info,
                        'current_method': current_methods[method_name]
                    })
                else:
                    # Method is inherited
                    hierarchy['inherited_methods'].append({
                        'name': method_name,
                        'from_class': base_class['name'],
                        'method_info': method_info
                    })
        
        return hierarchy